package dao;

import entity.FileMeta;

import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

// 通过这个类来封装针对 file_meta 表的操作
public class FileDao {

    /**
     *  1. 初始化数据库表（建表）
     */
    public void initDB() {
        // （1）先读取到 db.sql 中的 SQL 语句
        // （2）根据 SQL 语句调用到 jdbc 执行操作
        Connection connection = null;
        Statement statement = null;   // Statement 类是用于送 sql 语句到后端数据库的
        try {
            connection = DBUtil.getConnection();
            statement = connection.createStatement();
            String[] sqls = getInitSQL();
            for(String sql : sqls) {
                System.out.println("[initBD] sql: " + sql);
                statement.executeUpdate(sql);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(connection, statement, null);
        }
    }

    /**
     * 从 db.sql 中读取文件内容
     * 虽然当前 db.sql 只有一个 SQL语句，但是原则上写多个语句也是支持的
     * @return 返回值就设定成一个 String[]，每个元素都是一个独立的 SQL 语句
     */
    private String[] getInitSQL() {
        // 用 stringBuilder 来存储最终结果
        StringBuilder stringBuilder = new StringBuilder();
        // 此处需要能够动态获取到 db.sql 文件的路径，而不是直接写是一个绝对路径
        try {
            // 使用字节流读取 db.sql
            InputStream inputStream = FileDao.class.getClassLoader().
                    getResourceAsStream("db.sql");
            // 因为 db.sql 里边存放的是文本，所以需要将字节流（存放二进制）转换为字符流（存放文本）
            try(InputStreamReader inputStreamReader =
                        new InputStreamReader(inputStream, "utf8")) {
                while (true) {
                    // 注意：这里的 ch 返回值，虽然是 int，但是实际上是一个 char
                    // 只不过是为了表示 -1 这个特殊的结束标记，使用 int 来作为返回值
                    int ch = inputStreamReader.read();  // 读取到一个字符
                    if(ch == -1) {
                        break;  // 文件读取完毕，退出循环
                    }
                    stringBuilder.append((char) ch);  // 转换为 char 并添加到 stringBuilder 中
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 因为 db.sql 的每一条SQL语句都是以 ; 结尾
        // 所以 String[]，每个元素都是一个独立的 SQL 语句
        return stringBuilder.toString().split(";");
    }

    /**
     * 2. 插入文件/目录数据到数据库中
     *    这里提供一个 “批量插入” 的方法
     * @param fileMetas 需要批量处理的 文件/目录
     */
    public void add(List<FileMeta> fileMetas) {
        Connection connection = null;
        PreparedStatement preparedStatement = null;

        try {
            // 与数据库建立连接
            connection = DBUtil.getConnection();
            // 关闭数据库的自动连接功能，因为每一次 getConnection 操作都会执行一个 execute 系列的方法，
            // 都会和数据库进行交互一次（包括发起请求、返回响应、对数据库加锁解锁等操作）
            connection.setAutoCommit(false);
            String sql = "insert into file_meta values(null, ?, ?, ?, ?, ?, ?, ?)"; // 创建 SQL
            preparedStatement = connection.prepareStatement(sql); // 将 SQL 送往后端

            for(FileMeta fileMeta : fileMetas) {
                // 针对当前的 FileMeta 对象，替换到 SQL 语句中，把所有的 ? 替换成具体的数值
                preparedStatement.setString(1, fileMeta.getName());
                preparedStatement.setString(2, fileMeta.getPath());
                preparedStatement.setBoolean(3, fileMeta.isDirectory());
                preparedStatement.setString(4, fileMeta.getPinyin());
                preparedStatement.setString(5, fileMeta.getPinyinFirst());
                preparedStatement.setLong(6, fileMeta.getSize());
                preparedStatement.setTimestamp(7, new Timestamp(fileMeta.getLastModified()));
                // 使用 addBatch，把这个构造好的片段，累计起来
                // addBatch 会把已经构造好的 SQL 保存好，同时又会允许重新构造一个新的 SQL 出来
                preparedStatement.addBatch();  // 始终都是只有一个 preparedStatement

                // 打印插入日志。  File.separator 的作用相当于 ' \ '
                System.out.println("[insert] 插入文件：" + fileMeta.getPath() +
                        File.separator + fileMeta.getName());
            }

            // 执行所有的 SQL 片段
            preparedStatement.executeBatch();
            // 执行完毕之后，通过 commit 告知数据库，添加完毕
            connection.commit();
        } catch (SQLException e) {
            // 如果上述代码出现异常，就需要进行回滚操作
            try {
                if(connection != null) {
                    // 当 connection 建立没有失败的情况下，上述代码出现异常再回滚
                    // 如果连建立都失败了，那么就没必要回滚了
                    connection.rollback();
                }
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        } finally {
            DBUtil.close(connection, preparedStatement, null);
        }
    }

    /**
     * 3. 按照特定的关键词进行查询
     *    这个是在实现文件搜索功能的时候，必备的部分
     * @param pattern   此处查询的这个 pattern，可能是 文件名 的一部分，也可能是 文件名全拼音 的一部分，
     *                  还可能是 拼音首字母 的一部分
     * @return
     */
    public List<FileMeta> searchByPattern(String pattern) {
        List<FileMeta> fileMetas = new ArrayList<>();

        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;

        try {
            connection = DBUtil.getConnection();
            // 这里需要看 pattern 到底是 文件名、文件名全拼音、拼音首字母 哪个的一部分，需要对这3个进行模糊查询
            String sql = "select name, path, is_directory, size, last_modified from file_meta"
                    + " where name like ? or pinyin like ? or pinyin_first like ?"
                    + " order by path, name";
            statement = connection.prepareStatement(sql);
            statement.setString(1, "%" + pattern + "%");
            statement.setString(2, "%" + pattern + "%");
            statement.setString(3, "%" + pattern + "%");
            resultSet = statement.executeQuery();
            while (resultSet.next()) {
                String name = resultSet.getString("name");
                String path = resultSet.getString("path");
                boolean is_directory = resultSet.getBoolean("is_directory");
                long size = resultSet.getLong("size");
                Timestamp last_modified = resultSet.getTimestamp("last_modified");
                FileMeta fileMeta = new FileMeta(name, path, is_directory, size, last_modified.getTime());
                fileMetas.add(fileMeta);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(connection, statement, resultSet);
        }
        return fileMetas;
    }

    // 4. 给定路径，查询这个路径对应的结果，（这个路径都有哪些文件）
    //    这个方法，在后续重新扫描，更新数据库的时候用到
    public List<FileMeta> searchByPath(String targetPath) {
        List<FileMeta> fileMetas = new ArrayList<>();

        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;

        try {
            connection = DBUtil.getConnection();
            String sql = "select name, path, is_directory, size, last_modified from file_meta"
                    + " where path = ?";
            statement = connection.prepareStatement(sql);
            statement.setString(1, targetPath);
            resultSet = statement.executeQuery();
            while (resultSet.next()) {
                String name = resultSet.getString("name");
                String path = resultSet.getString("path");
                boolean isDirectory = resultSet.getBoolean("is_directory");
                long size = resultSet.getLong("size");
                Timestamp lastModified = resultSet.getTimestamp("last_modified");
                FileMeta fileMeta = new FileMeta(name, path, isDirectory, size, lastModified.getTime());
                fileMetas.add(fileMeta);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(connection, statement, resultSet);
        }
        return fileMetas;
    }

    /**
     *  5. 删除数据
     *     发现某一个文件已经从磁盘上删掉了，此时就需要把表里的内容也进行更新
     *     进行删除的时候，可能当前被删除的是一个普通文件（直接删除对应的表记录就行了）
     *     也有可能删除的时候，就删除的是一个目录，此时把目录里包含的子文件/子目录也都统一删除掉
     * @param fileMetas 要删除的 文件/目录
     */
    public void delete(List<FileMeta> fileMetas) {
         Connection connection = null;
         PreparedStatement statement = null;
        try {
            connection = DBUtil.getConnection();
            connection.setAutoCommit(false); // 关闭自动提交事务

            // 此处构造的 SQL，要根据当前删除的内容情况，来区分对待
            for(FileMeta fileMeta : fileMetas) {
                String sql = null;
                if(!fileMeta.isDirectory()) {
                    // 针对普通文件的删除操作
                    sql = "delete from file_meta where name = ? and path = ?";
                } else {
                    /*
                    针对目录的删除操作 ---》例如：当前要删除的 path 为 d:/test
                    此处的 path like ? 要替换成 'd:/test%' =》目的就是把当前这个被删除的目录下面的子文件和子目录都删掉
                     */
                    sql = "delete from file_meta where (name = ? and path = ?) or (path like ?)";
                }
                // 此处不能像之前的 add 一样，使用 addBatch，addBatch 的前提是：sql 是一个模板，是相同的，只是？替换的内容不同
                // 把 ？替换成不同的值，此处的 sql 是不一定相同的
                // 因此需要重新构造出 statement 对象来表示这个 SQL，因为 if，else只执行一个，所以每次都重新构建一个新的 statement
                statement = connection.prepareStatement(sql);
                if(!fileMeta.isDirectory()) {
                    // 普通文件，需要替换 2 个 ?
                    statement.setString(1, fileMeta.getName());
                    statement.setString(2, fileMeta.getPath());
                } else {
                    // 针对目录，需要替换 3 个 ?
                    statement.setString(1, fileMeta.getName());
                    statement.setString(2, fileMeta.getPath());
                    // File.separator 表示 斜杠（/）
                    statement.setString(3, fileMeta.getPath() + File.separator
                            + fileMeta.getName() + File.separator + "%");
                }
                // 真正执行这里的删除操作
                statement.executeUpdate();
                System.out.println("[delete] " + fileMeta.getPath() + File.separator  + fileMeta.getName());
                // 注意：因为上述的 sql 可能不同，可能删除文件，可能删除目录
                // 所以，statement 不同，此处代码中是有多个 statement 对象的。所以每个对象都得关闭一次
                statement.close();
            }

            // 告诉数据库，事务执行完毕
            connection.commit();

        } catch (SQLException e) {
            try {
                connection.rollback(); // 出现异常，回滚事务
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        } finally {
            DBUtil.close(connection, null, null);
        }
    }
}
